﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Text;
using System.Runtime.InteropServices;
using System.Collections.Specialized;
using System.Threading;
using Musicman.Utilities.Configuration.Search;
using System.IO;

namespace Musicman.Utilities
{
    public delegate void FileFindProgerssEventHandler(FileFindProgerssChangedEventArgs e);
    public delegate void ErrorEventHandler(FileFindErrorEventArgs e);
    public delegate void FileFindCompletedEventHandler(object sender, FileFindCompletedEventArgs e);
   

    /// <summary>
    /// The class responsible for asynchronous file finding
    /// </summary>
    public partial class FileFinder : Component
    {
        #region Interop and costants

        [DllImport("kernel32")]
        static extern int GetDriveType(string path);

        protected const int DRIVE_CDROM = 5;
        protected const int DRIVE_FIXED = 3;
        protected const int DRIVE_NO_ROOT = 1;
        protected const int DRIVE_REMOTE = 4;
        protected const int DRIVE_REMOVABLE = 2;

        /// <summary>Enumerate shares (NT)</summary>
        [DllImport("netapi32", CharSet = CharSet.Unicode)]
        protected static extern int NetShareEnum(string lpServerName, int dwLevel,
            out IntPtr lpBuffer, int dwPrefMaxLen, out int entriesRead,
            out int totalEntries, ref int hResume);

        /// <summary>Free the buffer (NT)</summary>
        [DllImport("netapi32")]
        protected static extern int NetApiBufferFree(IntPtr lpBuffer);

        /// <summary>Maximum path length</summary>
        protected const int MAX_PATH = 300;
        /// <summary>No error</summary>
        protected const int NO_ERROR = 0;
        /// <summary>Access denied</summary>
        protected const int ERROR_ACCESS_DENIED = 5;
        /// <summary>Access denied</summary>
        protected const int ERROR_WRONG_LEVEL = 124;
        /// <summary>More data available</summary>
        protected const int ERROR_MORE_DATA = 234;
        /// <summary>Not connected</summary>
        protected const int ERROR_NOT_CONNECTED = 2250;
        /// <summary>Level 1</summary>
        protected const int UNIVERSAL_NAME_INFO_LEVEL = 1;
        /// <summary>Max extries (9x)</summary>
        protected const int MAX_SI50_ENTRIES = 20;

        [Flags]
        public enum ShareType
        {
            /// <summary>Disk share</summary>
            Disk = 0,
            /// <summary>Printer share</summary>
            Printer = 1,
            /// <summary>Device share</summary>
            Device = 2,
            /// <summary>IPC share</summary>
            IPC = 3,
            /// <summary>Special share</summary>
            Special = -2147483648, // 0x80000000,
        }

        /// <summary>Share information, NT, level 1</summary>
        /// <remarks>
        /// Fallback when no admin rights.
        /// </remarks>
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        protected struct SHARE_INFO_1
        {
            [MarshalAs(UnmanagedType.LPWStr)]
            public string NetName;
            public ShareType ShareType;
            [MarshalAs(UnmanagedType.LPWStr)]
            public string Remark;
        }

        #endregion 

        private delegate void WorkerEventHandler(SearchJobConfigurationElement config, List<string> locations, AsyncOperation asyncOp);
        private SendOrPostCallback onProgressReportDelegate;
        private SendOrPostCallback onErrorDelegate;
        private SendOrPostCallback onCompletedDelegate;

        private HybridDictionary userStateToLifetime = new HybridDictionary();
        
        private AutoResetEvent workerThreadsAre = new AutoResetEvent(false);
        private AutoResetEvent mainThreadAre = new AutoResetEvent(false);

        private long workingThreadsCount = 0;
        private int maxWorkingThreads = 10;
        private int maxWaitTimeMinPerLocation = 10;

        public int MaxWorkingThreads
        {
            get { return maxWorkingThreads; }
            set { maxWorkingThreads = value; }
        }

        public int MaxWaitTimeMinPerLocation
        {
            get { return maxWaitTimeMinPerLocation; }
            set { maxWaitTimeMinPerLocation = value; }
        }

        /////////////////////////////////////////////////////////////
        #region Public events

        public event FileFindProgerssEventHandler ProgressChanged;
        public event ErrorEventHandler Error;
        public event FileFindCompletedEventHandler FileFindCompleted;

        #endregion

        /////////////////////////////////////////////////////////////
        #region Construction and destruction
        public FileFinder()
        {
            InitializeComponent();
            InitializeDelegates();
        }

        public FileFinder(IContainer container)
        {
            container.Add(this);
            InitializeComponent();
            InitializeDelegates();
        }

        protected virtual void InitializeDelegates()
        {
            onProgressReportDelegate =
                new SendOrPostCallback(ReportProgress);

            onErrorDelegate =
                new SendOrPostCallback(ReportError);

            onCompletedDelegate =
                new SendOrPostCallback(SearchCompleted);
            
        }
        #endregion

        /////////////////////////////////////////////////////////////
        #region Implementation
        public virtual void SearchFilesAsync(SearchJobConfigurationElement config, List<string> locations, object taskId)
        {
            // Create an AsyncOperation for taskId.
            AsyncOperation asyncOp =
                AsyncOperationManager.CreateOperation(taskId);

            // Multiple threads will access the task dictionary,
            // so it must be locked to serialize access.
            lock (userStateToLifetime.SyncRoot)
            {
                if (userStateToLifetime.Contains(taskId))
                {
                    throw new ArgumentException(
                        "Task ID parameter must be unique",
                        "taskId");
                }
                userStateToLifetime[taskId] = new AsyncJob(asyncOp);
            }

            // Start the asynchronous operation.
            WorkerEventHandler workerDelegate = new WorkerEventHandler(FileFindWorker);
            workerDelegate.BeginInvoke(
                config,
                locations,
                asyncOp,
                null,
                null);
        }

        // Utility method for determining if a 
        // task has been canceled.
        private bool TaskCanceled(object taskId)
        {
            return (userStateToLifetime[taskId] == null);
        }

        // This method cancels a pending asynchronous operation.
        public void CancelAsync(object taskId)
        {
            lock (userStateToLifetime.SyncRoot)
            {
                AsyncJob asyncJob = userStateToLifetime[taskId] as AsyncJob;
                if (asyncJob != null)
                {
                    userStateToLifetime.Remove(taskId);
                }
            }
        }

        private void FileFindWorker(SearchJobConfigurationElement config, List<string> locations, AsyncOperation asyncOp)
        {
            FileFindProgerssChangedEventArgs e = new FileFindProgerssChangedEventArgs(0, 
                    asyncOp.UserSuppliedState,
                    null,
                    null,
                    SearchJobStatus.JobStarted,
                    new TimeSpan());
            ReportProgress(e);

            WaitCallback wcb = new WaitCallback(SearchOneLocation);
            DateTime startTime = DateTime.Now;
            string[] criteria = config.Criteria.Split(';');
            // synchronizing the max working threads
            for (int i = 0; i < locations.Count; i++)
            {
                SearchOneLocationArgs args = new SearchOneLocationArgs(
                    config.SearchType, 
                    locations[i], 
                    criteria,
                    locations.Count, 
                    startTime, 
                    asyncOp,
                    config.MinFileSize,
                    config.MaxFileSize);
                ThreadPool.QueueUserWorkItem(wcb, args);
            }
            mainThreadAre.WaitOne();

            while (Interlocked.Read(ref workingThreadsCount) > 0)
            {
                mainThreadAre.WaitOne();
            }
        }

        private void SearchOneLocation(object searchOneLocationArgs)
        {
            while (Interlocked.Read(ref workingThreadsCount) > maxWorkingThreads)
            {
                workerThreadsAre.WaitOne();
            }
            Interlocked.Increment(ref workingThreadsCount);
            SearchOneLocationArgs args = searchOneLocationArgs as SearchOneLocationArgs;
            AsyncOperation asyncOperation = args.UserSuppliedState as AsyncOperation;
            Musicman.Common.LogManager.LogInfo(String.Format("{0}_{1} started", asyncOperation.UserSuppliedState, args.SearchLocation));

            if (TaskCanceled(asyncOperation.UserSuppliedState))
            {
                Musicman.Common.LogManager.LogInfo(String.Format("{0}_{1} task canceled", asyncOperation.UserSuppliedState, args.SearchLocation));
                return;
            }
            AsyncJob job = null;
            lock (userStateToLifetime.SyncRoot)
            {
                job = userStateToLifetime[asyncOperation.UserSuppliedState] as AsyncJob;
            }

            DirectoryInfo[] dirs = null;
            if (args.SearchType == SearchType.Local)
            {
                dirs = new DirectoryInfo(args.SearchLocation).GetDirectories();
            }
            else
            {
                dirs = GetNetShares(args.SearchLocation, args.UserSuppliedState);
            }

            DateTime startTime = DateTime.Now;
            List<FileInfo> files = new List<FileInfo>();
            for (int i = 0; i < dirs.Length; i++)
            {
                GetAllFiles(dirs[i], args.Criteria, args.MinSize, args.MaxSize, files, startTime, args.UserSuppliedState);
            }
            DateTime endTime = DateTime.Now;
            TimeSpan ts = new TimeSpan(endTime.Ticks - startTime.Ticks);
            Musicman.Common.LogManager.LogInfo(String.Format("{0}_{1} files retreived ", asyncOperation.UserSuppliedState, args.SearchLocation));

            if (job != null)
            {
                job.CurrentLocation++;
                job.TotalFiles += files.Count;
                int current = job.CurrentLocation;

                float percentage = (float)current / (float)args.Total * 100f;

                FileFindProgerssChangedEventArgs e = new FileFindProgerssChangedEventArgs(
                    (int)percentage,
                    asyncOperation.UserSuppliedState,
                    args.SearchLocation,
                    files.ToArray(),
                    ts.Minutes > maxWaitTimeMinPerLocation ? SearchJobStatus.SearchLocatoinTimedOut : SearchJobStatus.SearchLocationFinished,
                    ts);

                asyncOperation.Post(onProgressReportDelegate, e);
                if (current == args.Total || TaskCanceled(asyncOperation.UserSuppliedState))
                {
                    FileFindCompletedEventArgs cargs = new FileFindCompletedEventArgs(
                        null,
                        TaskCanceled(asyncOperation.UserSuppliedState),
                        asyncOperation.UserSuppliedState,
                        job.TotalFiles,
                        args.StartTime,
                        DateTime.Now);

                    asyncOperation.Post(onCompletedDelegate, cargs);
                    lock (userStateToLifetime.SyncRoot)
                    {
                        if (!TaskCanceled(asyncOperation.UserSuppliedState))
                        {
                            userStateToLifetime.Remove(asyncOperation.UserSuppliedState);
                        }
                    }
                }
            }
            else
            {
                Musicman.Common.LogManager.LogInfo("you have a problem this should never happen");
            }
            Interlocked.Decrement(ref workingThreadsCount);
            workerThreadsAre.Set();
            mainThreadAre.Set();
        }
        
        #endregion

        // This method is invoked via the AsyncOperation object,
        // so it is guaranteed to be executed on the correct thread.
        private void ReportProgress(object state)
        {
            FileFindProgerssChangedEventArgs e =
                state as FileFindProgerssChangedEventArgs;
            OnProgressChanged(e);
        }

        protected void OnProgressChanged(FileFindProgerssChangedEventArgs e)
        {
            if (ProgressChanged != null)
            {
                ProgressChanged(e);
            }
        }

        private void ReportError(object state)
        {
            FileFindErrorEventArgs e =
                state as FileFindErrorEventArgs;
            OnError(e);
        }

        private void OnError(FileFindErrorEventArgs e)
        {
            if (Error != null)
            {
                Error(e);
            }
        }

        private void SearchCompleted(object state)
        {
            FileFindCompletedEventArgs e = 
                state as FileFindCompletedEventArgs;
            OnFileFindCompleted(e);
        }

        private void OnFileFindCompleted(FileFindCompletedEventArgs e)
        {
            if (FileFindCompleted != null)
            {
                FileFindCompleted(this, e);
            }
        }

        /////////////////////////////////////////////////////////////
        #region Additional working methods

        private List<String> GetDrivesList()
        {
            List<string> res = new List<string>();
            string[] drives = Directory.GetLogicalDrives();
            for (int i = 0; i < drives.Length; i++)
            {
                Console.WriteLine(drives[i]);
                if (GetDriveType(drives[i]) == DRIVE_FIXED)
                {
                    res.Add(drives[i]);
                }
            }
            return res;
        }

        private DirectoryInfo[] GetNetShares(string searchLocation, object userSuppliedState)
        {
            int level = 1;
            int entriesRead, totalEntries, nRet, hResume = 0;
            IntPtr pBuffer = IntPtr.Zero;
            List<DirectoryInfo> directories = new List<DirectoryInfo>();
            try
            {
                nRet = NetShareEnum(searchLocation, level, out pBuffer, -1,
                out entriesRead, out totalEntries, ref hResume);
                if (nRet == NO_ERROR && entriesRead > 0)
                {
                    Type t = typeof(SHARE_INFO_1);
                    int offset = Marshal.SizeOf(t);
                    for (int i = 0, lpItem = pBuffer.ToInt32(); i < entriesRead; i++, lpItem += offset)
                    {
                        IntPtr pItem = new IntPtr(lpItem);
                        SHARE_INFO_1 si = (SHARE_INFO_1)Marshal.PtrToStructure(pItem, t);
                        if (si.ShareType == ShareType.Disk)
                        {
                            try
                            {
                                directories.Add(new DirectoryInfo(searchLocation + "\\" + si.NetName));
                            }
                            catch (Exception ex)
                            {
                                AsyncOperation asyncOperation = userSuppliedState as AsyncOperation;
                                FileFindErrorEventArgs e = new FileFindErrorEventArgs(
                                    ex, 
                                    String.Format("Error retreiving shares for the following location: {0}", searchLocation));
                                asyncOperation.Post(onErrorDelegate, e);
                            }
                        }
                    }
                }
            }
            finally
            {
                // Clean up buffer allocated by system
                if (IntPtr.Zero != pBuffer)
                    NetApiBufferFree(pBuffer);
            }
            return directories.ToArray();
        }

        private void GetAllFiles(DirectoryInfo directory, string[] criteria, long minSize, long maxSize, List<FileInfo> files, DateTime startTime, object userSuppliedState)
        {
            AsyncOperation asyncOperation = userSuppliedState as AsyncOperation;
            TimeSpan elapsedTime = new TimeSpan(DateTime.Now.Ticks - startTime.Ticks);
            if (TaskCanceled(asyncOperation.UserSuppliedState) || elapsedTime.Minutes > maxWaitTimeMinPerLocation)
            {
                return;
            }

            try
            {
                for (int i = 0; i < criteria.Length; i++)
                {
                    FileInfo[] newFiles = directory.GetFiles(criteria[i]);
                    if (minSize == Int64.MinValue && maxSize == Int64.MaxValue)
                    {
                        files.AddRange(newFiles);
                    }
                    else
                    {
                        foreach (FileInfo fi in newFiles)
                        {
                            if (fi.Exists &&
                                (minSize == Int64.MinValue || fi.Length > minSize) &&
                                (maxSize == Int64.MaxValue || fi.Length < maxSize))
                            {
                                files.Add(fi);
                            }
                        }
                    }
                }
                DirectoryInfo[] directories = directory.GetDirectories();
                for (int i = 0; i < directories.Length; i++)
                {
                    GetAllFiles(directories[i], criteria, minSize, maxSize, files, startTime, userSuppliedState);
                }
            }
            catch (Exception ex)
            {
                FileFindErrorEventArgs e = new FileFindErrorEventArgs(
                    ex,
                    String.Format("Error retreiving files for the following directory: {0}", directory.FullName));
                asyncOperation.Post(onErrorDelegate, e);
            }
        }
        #endregion
    }

    #region Arguments and helper classes
    public class FileFindProgerssChangedEventArgs : ProgressChangedEventArgs
    {
        private string searchLocation;
        private FileInfo[] files;
        private SearchJobStatus status;
        private TimeSpan elapsedTime;

        public FileFindProgerssChangedEventArgs(
            int percentage, 
            object userToken, 
            string searchLocation, 
            FileInfo[] files, 
            SearchJobStatus status,
            TimeSpan ts)
            : base(percentage, userToken)
        {
            this.searchLocation = searchLocation;
            this.files = files;
            this.status = status;
            this.elapsedTime = ts;
        }

        public string SearchLocation
        {
            get { return searchLocation; }
        }

        public FileInfo[] Files
        {
            get { return files; }
        }

        public SearchJobStatus Status
        {
            get { return status; }
        }
        public TimeSpan ElapsedTime
        {
            get { return elapsedTime; }
        }
    }

    public class FileFindErrorEventArgs : ErrorEventArgs
    {
        private string description;
        private Exception exception;

        public FileFindErrorEventArgs(Exception ex, string description) 
            : base(ex)
        {
            this.description = description;
            this.exception = ex;
        }
        
        public string Description
        {
            get { return description; }
        }

        public Exception Exception
        {
            get { return exception; }
        }
    }

    public class FileFindCompletedEventArgs : AsyncCompletedEventArgs
    {
        private long totalFiles;
        private DateTime startTime;
        private DateTime endTime;

        public FileFindCompletedEventArgs(
            Exception e, 
            bool cancelled, 
            object state, 
            long totalFiles, 
            DateTime startTime, 
            DateTime endTime)
            : base(e, cancelled, state)
        {
            this.totalFiles = totalFiles;
            this.startTime = startTime;
            this.endTime = endTime;
        }

        public long TotalFiles
        {
            get { return totalFiles; }
        }

        public DateTime StartTime
        {
            get { return startTime; }
        }

        public DateTime EndTime
        {
            get { return endTime; }
        }
    }

    public enum SearchJobStatus : byte
    {
        JobStarted,
        SearchLocationFinished,
        SearchLocatoinTimedOut,
        JobFinished
    }

    internal class SearchOneLocationArgs
    {
        private SearchType searchType;
        private string searchLocation;
        private string[] criteria;
        private int total;
        private DateTime startTime;
        private long minSize;
        private long maxSize;
        private object userSuppliedState;

        public SearchOneLocationArgs(
            SearchType searchType, 
            string location, 
            string [] criteria,
            int total, 
            DateTime startTime, 
            object userSupliedState,
            long minSize,
            long maxSize)
        {
            this.searchType = searchType;
            this.searchLocation = location;
            this.criteria = criteria;
            this.total = total;
            this.startTime = startTime;
            this.userSuppliedState = userSupliedState;
            this.minSize = minSize;
            this.maxSize = maxSize;
        }

        public string SearchLocation
        {
            get { return searchLocation; }
        }
        public string[] Criteria
        {
            get { return criteria; }
        }
        public SearchType SearchType
        {
            get { return searchType; }
        }
        public int Total
        {
            get { return total; }
        }
        public object UserSuppliedState
        {
            get { return userSuppliedState; }
        }
        public DateTime StartTime
        {
            get { return startTime; }
        }
        public long MinSize
        {
            get { return minSize; }
            set { minSize = value; }
        }
        public long MaxSize
        {
            get { return maxSize; }
            set { maxSize = value; }
        }
    }

    internal class AsyncJob
    {
        private AsyncOperation operation;
        private int currentLocation = 0;
        private long totalFiles = 0;

        public AsyncJob(AsyncOperation operation)
        {
            this.operation = operation;
        }

        public AsyncOperation Operation
        {
            get { return operation; }
        }
        public int CurrentLocation
        {
            get { return currentLocation; }
            set { currentLocation = value; }
        }
        public long TotalFiles
        {
            get { return totalFiles; }
            set { totalFiles = value; }
        }
    }
    #endregion 
}
